package com.wontube.media;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;

public final class MP3Writer implements IAudioWriter {
	private File file;
	private RandomAccessFile fs;
	private List<String> warnings;
	private List<byte[]> chunkBuffer;
	private List<Long> frameOffsets;
	private long totalFrameLength;
	private boolean isVBR;
	private boolean delayWrite;
	private boolean hasVBRHeader;
	private boolean writeVBRHeader;

	private int firstBitRate;
	private int mpegVersion;
	private int sampleRate;
	private int channelMode;
	private long firstFrameHeader;

	public MP3Writer(String path, List<String> warnings) throws IOException {
		this.file = new File(path);
		this.fs = new RandomAccessFile(this.file, "rw");
		this.warnings = warnings;
		this.chunkBuffer = new ArrayList<byte[]>();
		this.frameOffsets = new ArrayList<Long>();
		this.delayWrite = true;
	}

	public void writeChunk(byte[] chunk, long timeStamp) throws IOException {
		this.chunkBuffer.add(chunk);
		this.parseMP3Frames(chunk);

		if (this.delayWrite && this.totalFrameLength >= 65536) {
			this.delayWrite = false;
		}
		
		if (!this.delayWrite) {
			this.flush();
		}
	}

	public void finish() throws IOException {
		this.flush();

		try {
			if (this.writeVBRHeader) {
				this.fs.seek(0L);
				this.writeVBRHeader(false);
			}
		} finally {
			this.fs.close();
		}
	}

	public File getFile() {
		return this.file;
	}

	private void flush() throws IOException {
		for (byte[] chunk : this.chunkBuffer) {
			this.fs.write(chunk, 0, chunk.length);
		}
		this.chunkBuffer.clear();
	}

	private void parseMP3Frames(byte[] buff) throws IOException {
		int[] mpeg1Bitrate = new int[] { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 };
		int[] mpeg2XBitrate = new int[] { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 };
		int[] mpeg1SampleRate = new int[] { 44100, 48000, 32000, 0 };
		int[] mpeg20SampleRate = new int[] { 22050, 24000, 16000, 0 };
		int[] mpeg25SampleRate = new int[] { 11025, 12000, 8000, 0 };

		int offset = 0;
		int length = buff.length;

		while (length >= 4) {
			int mpegVersion, layer, bitRate, sampleRate, padding, channelMode;
			int frameLen;

			long header = (long) BitConverterBE.toUInt32(buff, offset) << 32;
			long[] headers = new long[] { header };
			if (BitHelper.read(headers, 11) != 0x7FF) {
				break;
			}

			mpegVersion = BitHelper.read(headers, 2);
			layer = BitHelper.read(headers, 2);
			BitHelper.read(headers, 1);
			bitRate = BitHelper.read(headers, 4);
			sampleRate = BitHelper.read(headers, 2);
			padding = BitHelper.read(headers, 1);
			BitHelper.read(headers, 1);
			channelMode = BitHelper.read(headers, 2);

			if ((mpegVersion == 1) || (layer != 1) || (bitRate == 0) || (bitRate == 15) || (sampleRate == 3)) {
				break;
			}

			bitRate = ((mpegVersion == 3) ? mpeg1Bitrate[bitRate] : mpeg2XBitrate[bitRate]) * 1000;

			if (mpegVersion == 3)
				sampleRate = mpeg1SampleRate[sampleRate];
			else if (mpegVersion == 2)
				sampleRate = mpeg20SampleRate[sampleRate];
			else
				sampleRate = mpeg25SampleRate[sampleRate];

			frameLen = getFrameLength(mpegVersion, bitRate, sampleRate, padding);
			if (frameLen > length) {
				break;
			}

			boolean isVBRHeaderFrame = false;
			if (this.frameOffsets.size() == 0) {
				// Check for an existing VBR header just to be safe (I haven't
				// seen any in FLVs)
				int o = offset + getFrameDataOffset(mpegVersion, channelMode);
				if (BitConverterBE.toUInt32(buff, o) == 0x58696E67) { // "Xing"
					isVBRHeaderFrame = true;
					this.delayWrite = false;
					this.hasVBRHeader = true;
				}
			}

			if (isVBRHeaderFrame) {

			} else if (firstBitRate == 0) {
				firstBitRate = bitRate;
				this.mpegVersion = mpegVersion;
				this.sampleRate = sampleRate;
				this.channelMode = channelMode;
				firstFrameHeader = BitConverterBE.toUInt32(buff, offset);
			} else if (!isVBR && (bitRate != firstBitRate)) {
				isVBR = true;
				if (hasVBRHeader) {
				} else if (delayWrite) {
					this.writeVBRHeader(true);
					this.writeVBRHeader = true;
					this.delayWrite = false;
				} else {
					this.warnings.add("Detected VBR too late, cannot add VBR header.");
				}
			}

			this.frameOffsets.add(this.totalFrameLength + offset);

			offset += frameLen;
			length -= frameLen;
		}

		this.totalFrameLength += buff.length;
	}

	private void writeVBRHeader(boolean isPlaceholder) throws IOException {
		byte[] buff = new byte[getFrameLength(mpegVersion, 64000, this.sampleRate, 0)];
		if (!isPlaceholder) {
			long header = this.firstFrameHeader;
			int dataOffset = getFrameDataOffset(mpegVersion, this.channelMode);
			header &= 0xFFFE0DFF; // Clear CRC, bitrate, and padding fields
			header |= (int) ((mpegVersion == 3) ? 5 : 8) << 12; // 64 kbit/sec
			BitHelper.copyBytes(buff, 0, BitConverterBE.getBytes(header));
			BitHelper.copyBytes(buff, dataOffset, BitConverterBE.getBytes((long)0x58696E67)); // "Xing"
			BitHelper.copyBytes(buff, dataOffset + 4, BitConverterBE.getBytes((long)0x7)); // Flags
			BitHelper.copyBytes(buff, dataOffset + 8, BitConverterBE.getBytes((long)this.frameOffsets.size())); // Frame
																											// count
			BitHelper.copyBytes(buff, dataOffset + 12, BitConverterBE.getBytes((long)this.totalFrameLength)); // File
																										// length
			for (int i = 0; i < 100; i++) {
				int frameIndex = (int) ((i / 100.0) * this.frameOffsets.size());
				buff[dataOffset + 16 + i] = (byte) ((this.frameOffsets.get(frameIndex) / (double) totalFrameLength) * 256.0);
			}
		}
		this.fs.write(buff, 0, buff.length);
	}

	private int getFrameLength(int mpegVersion, int bitRate, int sampleRate, int padding) {
		return ((mpegVersion == 3) ? 144 : 72) * bitRate / sampleRate + padding;
	}

	private int getFrameDataOffset(int mpegVersion, int channelMode) {
		return 4 + ((mpegVersion == 3) ? ((channelMode == 3) ? 17 : 32) : ((channelMode == 3) ? 9 : 17));
	}
}
